#include <Kharlia/Engine/Common.h>
#include <Kharlia/Engine/TileData.h>
#include <Kharlia/Engine/Init.h>

namespace kharlia { namespace engine {

using namespace py;

tuple PSFTileData::getinitargs(FTileData& td) {
    assert(sizeof(tile_t) == 2 && sizeof(char) == 1);

    // We'll pickle the tiles as a python string
    const char* front = reinterpret_cast<const char*>(&td.GetTiles().front());

    return make_tuple(td.GetSize(), str(front, td.GetTiles().size() * 2));
}

FTileData::FTileData(const FSizeI& size):
    mSize(size),
    mTiles(size.Width * size.Height)
{
}

FTileData::FTileData(const FSizeI& size, object sequence):
    mSize(size),
    mTiles(size.Width * size.Height)
{
    SetTiles(FVector2I(0,0), size, sequence);
}

// Unpickling constructor
FTileData::FTileData(const FSizeI& size, str datastr):
    mSize(size),
    mTiles(size.Width * size.Height)
{
    SetTiles(FVector2I(0, 0), size, datastr);
}

FTileData::~FTileData() {
}

void FTileData::SetTiles(tile_t tile) {
    for (unsigned int i = 0; i < mTiles.size(); i++) {
        mTiles[i] = tile;
    }
}

void FTileData::SetTiles(const FVector2I& topos, const FSizeI& size, tile_t tile) {
    for (int h = 0; h < size.Height; h++) {
        for (int w = 0; w < size.Width; w++) {
            // TODO: Tile updating for tuples and lists
            FVector2I dest(w + topos.X, h + topos.Y);
            if (dest.X < 0 || dest.X >= mSize.Width ||
                dest.Y < 0 || dest.Y >= mSize.Height) {
                continue;
            }
            mTiles[(mSize.Width * dest.Y) + dest.X] = tile;
        }
    }
}
// TODO: Add fast operations for tuples and lists
void FTileData::SetTiles(const FVector2I& topos, const FSizeI& size, object sequence) {
    for (int h = 0; h < size.Height; h++) {
        for (int w = 0; w < size.Width; w++) {
            // TODO: Tile updating for tuples and lists
            FVector2I dest(w + topos.X, h + topos.Y);
            if (dest.X < 0 || dest.X >= mSize.Width ||
                dest.Y < 0 || dest.Y >= mSize.Height) {
                continue;
            }
            tile_t tile = extract<tile_t>(sequence[(size.Width * h) + w]);
            mTiles[(mSize.Width * dest.Y) + dest.X] = tile;
        }
    }
}

void FTileData::SetTilesFast(const FVector2I& topos, const FSizeI& size, object sequence) {
    if (PyList_CheckExact(sequence.ptr()) &&
        PyList_GET_SIZE(sequence.ptr()) > 0 &&
        PyInt_CheckExact(PyList_GET_ITEM(sequence.ptr(), 0)))
    {
        tile_t* tiles = &mTiles.front();
        PyObject* tilelist = sequence.ptr();
        for (int h = 0; h < size.Height; h++) {
            for (int w = 0; w < size.Width; w++) {
                FVector2I dest(w + topos.X, h + topos.Y);
                if (dest.X < 0 || dest.X >= mSize.Width ||
                    dest.Y < 0 || dest.Y >= mSize.Height) {
                    continue;
                }
                tiles[(mSize.Width * dest.Y) + dest.X] =
                    tile_t(PyInt_AS_LONG(PyList_GET_ITEM(tilelist, (size.Width * h) + w)));
            }
        }
    }
    else {
        throw RuntimeError("unsupported sequence type");
    }
}

void FTileData::SetTiles(const FVector2I& topos, const FSizeI& size, str tiledatastr) {
    tile_t* tiles = reinterpret_cast<tile_t*>(PyString_AS_STRING(tiledatastr.ptr()));
    // Verify that datastr length == size
    if ((len(tiledatastr) / 2) != (size.Width * size.Height)) {
        throw RuntimeError("size mismatch");
    }
    SetTiles(topos, size, tiles); 
}

// WARNING: VERY UNSAFE
void FTileData::SetTiles(const FVector2I& topos, const FSizeI& size, const tile_t* tiles) {
    for (int h = 0; h < size.Height; h++) {
        for (int w = 0; w < size.Width; w++) {
            // TODO: Tile updating for tuples and lists
            FVector2I dest(w + topos.X, h + topos.Y);
            if (dest.X < 0 || dest.X >= mSize.Width ||
                dest.Y < 0 || dest.Y >= mSize.Height) {
                continue;
            }
            mTiles[(mSize.Width * dest.Y) + dest.X] = tiles[(size.Width * h) + w];
        }
    }
}

void FTileData::CopyTiles(const FVector2I& destpos, shared_ptr<FTileData> src) {
    CopyTiles(destpos, FRectI(FVector2I(), src->GetSize()), src);
}

void FTileData::CopyTiles(const FVector2I& destpos, const FRectI& srcrect, shared_ptr<FTileData> src) {
    int srcx, srcy, srcw, srch, maxw, maxh,
        destx, desty, ddx, ddy;

    destx = destpos.X;
    desty = destpos.Y;
    // Clip source rectangle to source TileData
    srcx = srcrect.GetX();
    srcw = srcrect.GetWidth();
    if (srcx < 0) {
        srcw += srcx;
        destx -= srcx;
        srcx = 0;
    }
    maxw = src->GetSize().Width - srcx;
    if (maxw < srcw)
        srcw = maxw;

    srcy = srcrect.GetY();
    srch = srcrect.GetHeight();
    if (srcy < 0) {
        srch += srcy;
        desty -= srcy;
        srcy = 0;
    }
    maxh = src->GetSize().Height - srcy;
    if (maxh < srch)
        srch = maxh;
    // Clip dest rectangle to tiledata size
    if (destx < 0) {
        srcw += destx;
        srcx -= destx;
        destx = 0;
    }
    if (desty < 0) {
        srch += desty;
        srcy -= desty;
        desty = 0;
    }
    ddx = destx + srcw - mSize.Width;
    if (ddx > 0)
        srcw -= ddx;
    ddy = desty + srch - mSize.Height;
    if (ddy > 0)
        srch -= ddy;

    if (srcw > 0 && srch > 0) {
        CopyTilesImpl(FVector2I(destx, desty), FRectI(srcx, srcy, srcw, srch), src.get());
    }
}

void FTileData::CopyTilesImpl(const FVector2I& destpos, const FRectI& srcrect, FTileData* src) {
    const tile_t* srctiles = &src->mTiles.front();
    tile_t* desttiles = &mTiles.front();
    const FSizeI& srcsize = src->mSize;
    int srcindex = (srcsize.Width * srcrect.Pos.Y) + srcrect.Pos.X;
    int destindex = (mSize.Width * destpos.Y) + destpos.X;
    errno_t err;

    //format fmt("Copied tile row: destindex=%1%, srcindex=%2%, size=%3%");

    // Copy each row of tiles from source to dest
    for (int h = 0; h < srcrect.Size.Height; h++) {
        err = memcpy_s(desttiles + destindex, sizeof(tile_t) * srcrect.Size.Width,
                       srctiles + srcindex, sizeof(tile_t) * srcrect.Size.Width);
        if (err)
            break;

        //fmt % destindex % srcindex % srcrect.Size.Width;
        //GEngineLogger->Debug(fmt);

        srcindex += srcsize.Width;
        destindex += mSize.Width;
    }
    // Errors should be very unlikely
    switch (err) {
    case 0:
        break;
    case EINVAL:
        throw RuntimeError("invalid source or destination");
    case ERANGE:
        throw RuntimeError("out of range");
    }
}

list FTileData::ToList() const {
    list out;
    for (uint i = 0; i < mTiles.size(); i ++) {
        out.append(mTiles[i]);
    }
    return out;
}

object FTileData::ToString() const {
    if (mTiles.size() > 0) {
        return str(reinterpret_cast<const char*>(&mTiles.front()), mTiles.size() * sizeof(tile_t));
    }
    return object();
}

str FTileData::Repr() const {
    format fmt("<kharlia._engine.FTileData object at 0x%|3$08x|, width=%1%, height=%2%>");
    fmt % mSize.Width % mSize.Height % (uint32)this;
    return str(fmt.str());
}

void _InitTileDataClasses() {
    _InitTileDataBindings();
}

}} //namespace Kharlia
